Skip to content

fix(console-ui): fix hydration mismatch that broke client-side navigation (root cause)#463

Merged
anupsv merged 3 commits into
masterfrom
fix/verification-mode-hydration-nav
Jun 24, 2026
Merged

fix(console-ui): fix hydration mismatch that broke client-side navigation (root cause)#463
anupsv merged 3 commits into
masterfrom
fix/verification-mode-hydration-nav

Conversation

@anupsv

@anupsv anupsv commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

This is the root cause behind the broken navigation that #457/#458/#462 have been chasing/band-aiding: client-side next/link navigation silently fails app-wide (links render but router.push no-ops), so only full-page-load <a> works.

VerificationModeProvider — added in #450 and wrapping the entire app in layout.tsx — read localStorage inside its useState initializer:

const [mode, setMode] = useState<VerificationMode>(getInitialMode); // reads localStorage

So when a user had toggled technical mode, the first client render (technical) diverged from the server HTML (normal). On a React hydration mismatch the server DOM is discarded and the tree is regenerated on the client, which breaks App Router client navigation everywhere.

This is the exact bug class #457 fixed for the store and InviteCodeBanner — it just missed this provider (also from #450). #458 then worked around the symptom by switching the sidebar to native <a>.

Fix

Make the first render deterministic (matches the server), then load the persisted preference in an effect — the same pattern as #457:

const [mode, setMode] = useState<VerificationMode>("normal");
useEffect(() => {
  const stored = localStorage.getItem(STORAGE_KEY);
  if (stored === "technical" || stored === "normal") setMode(stored);
}, []);

Adds verification-mode.test.tsx pinning the contract: first render is normal even when localStorage says technical, the persisted value applies after mount, and toggle persists.

Scope / verification note

  • I swept every localStorage/window/Date/random read in the global render path (layoutAppShell and the providers it mounts). After this change, VerificationModeProvider was the only remaining render-time localStorage read; ThemeProvider, the store (fix(console-ui): sidebar nav unclickable — fix React #418 hydration mismatch #457), and InviteCodeBanner (fix(console-ui): sidebar nav unclickable — fix React #418 hydration mismatch #457) are already effect-based. ProviderSlackPopup also reads localStorage in its initializers but is masked-safe (its !isProvider gate makes the first render null on both server and client) — a latent instance of the same pattern worth hardening separately.
  • Could not run a browser repro here, so I kept the native-<a> band-aids in place. Please verify on the preview deploy with localStorage['darkbloom-verification-mode'] = 'technical' that next/link navigation works (no hydration error in console).

Follow-up (separate PR)

Once verified, the native-<a> workarounds can be reverted to restore SPA navigation: the sidebar (#458) and the provider tabs (#462 — keep its "hide tabs until installed" change, revert only the <a> swap), plus the 2 remaining <Link>s in stats/earn stay correct either way.

Test plan

  • npx vitest run — 3/3 in the new suite.
  • npx eslint — clean.
  • npm run build — succeeds.
  • Manual on preview: with technical mode set, no React hydration error on load; clicking next/link elements navigates.

Made with Cursor


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

…tion (root cause)

VerificationModeProvider — added in #450 and wrapping the entire app — read
localStorage in its useState initializer, so the first client render diverged
from the server HTML whenever the user had toggled "technical" mode. On a React
hydration mismatch the server DOM is discarded and the tree is regenerated on
the client, which breaks App Router client navigation app-wide: next/link renders
but router.push silently no-ops (links don't switch pages).

This is the regression #457 chased — it made the store and InviteCodeBanner
hydration-deterministic but missed this provider — and #458 then band-aided by
switching the sidebar to native <a>.

Fix: start "normal" on the server and the first client render, then load the
persisted preference in an effect (the same hydration-determinism pattern as
#457). Adds a regression test pinning the deterministic first render.

This is the root-cause fix for the broken Link client router; the native-<a>
workarounds (#458 sidebar, #462 provider tabs) can be reverted in a follow-up
once verified on a preview deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
d-inference Ready Ready Preview Jun 24, 2026 8:24pm
d-inference-console-ui-dev Ready Ready Preview Jun 24, 2026 8:24pm
d-inference-landing Ready Ready Preview Jun 24, 2026 8:24pm

Request Review

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

No threat-model-covered files were modified; this PR adds a UI verification-mode component and its test, which falls entirely outside the current threat model's affected_files mappings.


Trust boundaries touched

None of the existing TB-xxx boundaries are directly exercised by these two files.


New attack surface not covered by an existing threat

The new verification-mode.tsx component warrants a brief look before declaring it fully neutral:

  • What it likely does: based on the filename, this component probably renders some provider-trust or attestation-verification status to the user (e.g. "hardware trusted", "self-signed", trust level badges). If it reads trust/attestation state from the coordinator and renders it verbatim, it sits adjacent to TB-004 (browser → coordinator) and indirectly near TB-009 (attestation chain).

  • Risk if trust state is consumer-facing without caveats: if the component fetches or receives a trust-level string from the coordinator and renders it as a security claim with no server-side authorisation check on that fetch, it would be a UI surface for T-036 (trust level elevated without completing the MDM/MDA chain) — a user could be shown "hardware trusted" for a provider that only reached TrustSelfSigned. This is not a confirmed issue from the diff alone (no coordinator fetch is visible without seeing the component's data source), but it is the one pattern to verify in review.

  • Test file: verification-mode.test.tsx is a pure test with no runtime surface.


Recommendation for threat model maintainers

If verification-mode.tsx renders coordinator-sourced trust-level data to consumers, add it to the affected_files list of T-036 and/or T-033 and note TB-004 as a touched boundary. If it is purely cosmetic / local-state UI with no coordinator fetch, no model update is needed.

No SEC-* findings are resolved by this PR.


🔐 Threat model: docs/threat-model.yaml · Updates on each push to this PR

@blacksmith-sh

blacksmith-sh Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
github.com/eigeninference/d-inference/e2e/TestIntegration_ConcurrentRequests View Logs

Fix with Codesmith
Need help on this PR? Tag /codesmith with what you need.

@ethenotethan ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review — Layr-Labs/d-inference#

Verdict: COMMENT

Security — ✅ No issues found

Performance — ✅ No issues found

Type_diligence — ✅ No issues found

Additive_complexity — ✅ No issues found

✅ All four passes clean. No issues found.

🤖 Automated review by Centaur · DAR-186

anupsv added a commit that referenced this pull request Jun 24, 2026
…guardrails)

Adds a real-browser E2E layer (Playwright + Chromium) for the console UI. Unlike
the jsdom Vitest unit tests, these boot the app and drive an actual browser, so
they catch SSR-hydration and client-navigation regressions jsdom cannot.

- playwright.config.ts: hermetic mock-auth dev server (Privy unconfigured) — no
  coordinator or secrets needed.
- e2e/navigation.spec.ts: every shell route loads with no React hydration error;
  a persisted verification-mode preference hydrates cleanly; sidebar links and
  the provider-dashboard tabs switch routes.
- vitest.config.ts: exclude e2e/ so Vitest doesn't pick up the Playwright specs.
- npm scripts: test:e2e / test:e2e:ui.
- CI: new "Console UI E2E (Playwright)" job (installs chromium, runs the suite).

Honest scope note: this hermetic mock-auth harness does NOT reproduce the
production #463 hydration break (the verification-mode consumers need real
authenticated trust data to render divergent DOM), so the suite is a broad
hydration + navigation guardrail rather than proof of that specific fix.
Verified locally: 11/11 pass.

Co-authored-by: Cursor <cursoragent@cursor.com>

@ethenotethan ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review — Layr-Labs/d-inference#

Verdict: COMMENT

Security — ✅ No issues found

Performance — ✅ No issues found

Type_diligence — ✅ No issues found

Additive_complexity — ✅ No issues found

✅ All four passes clean. No issues found.

🤖 Automated review by Centaur · DAR-186

@ethenotethan ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review — Layr-Labs/d-inference#

Verdict: COMMENT

Security — ✅ No issues found

Performance — ✅ No issues found

Type_diligence — ✅ No issues found

Additive_complexity — ✅ No issues found

✅ All four passes clean. No issues found.

🤖 Automated review by Centaur · DAR-186

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants